/*******************************************************************************
 * Copyright (c) 2008, 2009 Tapestry IDE Developers
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors:
 *     Tapestry IDE Developers - initial API and implementation
 *******************************************************************************/
package com.googlecode.tapestry.ide.internal.core.model;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ProjectScope;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.IScopeContext;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;

import com.googlecode.tapestry.ide.core.TapestryFileType;
import com.googlecode.tapestry.ide.core.model.AbstractResourceModelElement;
import com.googlecode.tapestry.ide.core.model.AbstractTapestryItem;
import com.googlecode.tapestry.ide.core.model.ITapestryComponent;
import com.googlecode.tapestry.ide.core.model.ITapestryContainer;
import com.googlecode.tapestry.ide.core.model.ITapestryItem;
import com.googlecode.tapestry.ide.core.model.ITapestryModel;
import com.googlecode.tapestry.ide.core.model.ITapestryPage;
import com.googlecode.tapestry.ide.core.model.ITapestryProject;
import com.googlecode.tapestry.ide.internal.core.preferences.PreferenceConstants;

/**
 * This class holds information for a Tapestry project.
 */
public class TapestryProject extends AbstractResourceModelElement implements
		ITapestryProject {

	/** The project. */
	private IProject project;

	/** The java project. */
	private IJavaProject javaProject;

	/** Is <code>true</code> when the model is populated. */
	private boolean populated = false;

	/** The pages. */
	protected volatile ITapestryContainer<ITapestryPage> pages = new TapestryContainer<ITapestryPage>(
			this, "Pages");

	/** The components. */
	protected volatile ITapestryContainer<ITapestryComponent> components = new TapestryContainer<ITapestryComponent>(
			this, "Components");

	/** The preferences. */
	private IEclipsePreferences preferences;

	/** The initialized. */
	private boolean initialized = false;

	/**
	 * Instantiates a new tapestry project.
	 * 
	 * @param project
	 *            the project
	 * @param model
	 *            the model
	 */
	public TapestryProject(ITapestryModel model, IProject project) {
		super(model, project.getName());
		this.project = project;

		javaProject = JavaCore.create(project);

		IScopeContext context = new ProjectScope(getProject());
		preferences = context.getNode(PreferenceConstants.QUALIFIER);
	}

	/* (non-Javadoc)
	 * @see com.googlecode.tapestry.ide.core.model.ITapestryProject#isPopulated()
	 */
	public boolean isPopulated() {
		return populated;
	}

	/**
	 * Gets the tapestry root package.
	 * 
	 * @return the tapestry root package
	 */
	String getTapestryRootPackage() {
		return preferences.get(PreferenceConstants.APPLICATION_PACKAGE_KEY,
				PreferenceConstants.APPLICATION_PACKAGE_DEFAULT);
	}

	/**
	 * Checks if is initialized.
	 * 
	 * @return true, if is initialized
	 */
	public boolean isInitialized() {
		return initialized;
	}

	/**
	 * Inits the.
	 */
	public void init() {
		initialized = true;
		refresh();
	}

	/**
	 * Refresh.
	 */
	public void refresh() {
		String rootPackage = getTapestryRootPackage();
		try {
			for (IPackageFragmentRoot fragmentRoot : javaProject
					.getPackageFragmentRoots()) {
				if (isSourceRootIncluded(fragmentRoot)) {
					scanPackageFragmentRoot(fragmentRoot, rootPackage,
							TapestryItemType.COMPONENT);
					scanPackageFragmentRoot(fragmentRoot, rootPackage,
							TapestryItemType.PAGE);
				}
			}
		} catch (JavaModelException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		populated = true;
	}

	/**
	 * Scan package fragment root.
	 * 
	 * @param root
	 *            the root
	 * @param tapestryPkgName
	 *            the tapestry pkg name
	 * @param itemType
	 *            the item type
	 * 
	 * @throws JavaModelException
	 *             the java model exception
	 */
	void scanPackageFragmentRoot(IPackageFragmentRoot root,
			String tapestryPkgName, TapestryItemType itemType)
			throws JavaModelException {
		IJavaElement[] javaElements = (IJavaElement[]) root.getChildren();
		for (IJavaElement javaElement : javaElements) {
			if (javaElement instanceof IPackageFragment) {
				IPackageFragment pkg = (IPackageFragment) javaElement;
				if (pkg.getElementName().startsWith(
						tapestryPkgName + "." + itemType.getPackageName())) {
					for (ICompilationUnit cu : pkg.getCompilationUnits()) {
						IPath path = calculatePathToTapestryPackage(root, cu
								.getResource());
						addJava(cu, itemType, path, pkg.getResource()
								.getProjectRelativePath());
					}
				}
			}
		}
	}

	/**
	 * File added.
	 * 
	 * @param file
	 *            the file
	 * 
	 * @return the i tapestry item
	 * 
	 * @throws JavaModelException
	 *             the java model exception
	 */
	ITapestryItem fileAdded(IFile file) throws JavaModelException {
		IPackageFragmentRoot root = getPackageFragmentRoot(file);
		if (root != null) {
			IPath pathToTapestryPackage = calculatePathToTapestryPackage(root,
					file);
			if (pathToTapestryPackage != null) {
				TapestryItemType itemType = determineItemType(pathToTapestryPackage);
				ITapestryItem item = null;
				switch (TapestryFileType.fromResource(file)) {
				case JAVA:
					item = addJava(file, itemType, pathToTapestryPackage, root
							.getResource().getProjectRelativePath());
					break;
				case TML:
					item = addTemplate(file, itemType, pathToTapestryPackage);
					break;
				default:
					break;
				}
				return item;
			}
		}
		return null;
	}

	/**
	 * Adds the template.
	 * 
	 * @param templateFile
	 *            the template file
	 * @param itemType
	 *            the item type
	 * @param pathToTapestryPackage
	 *            the path to tapestry package
	 * 
	 * @return the i tapestry item
	 */
	ITapestryItem addTemplate(IFile templateFile, TapestryItemType itemType,
			IPath pathToTapestryPackage) {
		String name = pathToTapestryPackage.removeFirstSegments(1)
				.removeFileExtension().toString();
		ITapestryContainer<? extends ITapestryItem> container = getContainer(itemType);
		AbstractTapestryItem item = (AbstractTapestryItem) container
				.getItem(name);
		if (item != null) {
			item.setTemplateResource(templateFile);
		}
		return item;
	}

	/**
	 * Adds the java.
	 * 
	 * @param javaFile
	 *            the java file
	 * @param itemType
	 *            the item type
	 * @param pathToTapestryPackage
	 *            the path to tapestry package
	 * @param rootPackagePath
	 *            the root package path
	 * 
	 * @return the i tapestry item
	 * 
	 * @throws JavaModelException
	 *             the java model exception
	 */
	ITapestryItem addJava(IFile javaFile, TapestryItemType itemType,
			IPath pathToTapestryPackage, IPath rootPackagePath)
			throws JavaModelException {
		if (itemType != null) {
			ICompilationUnit compilationUnit = (ICompilationUnit) javaProject
					.findElement(javaFile.getProjectRelativePath()
							.makeRelativeTo(rootPackagePath));
			return addJava(compilationUnit, itemType, pathToTapestryPackage,
					rootPackagePath);
		}
		return null;
	}

	/**
	 * Adds the java.
	 * 
	 * @param compilationUnit
	 *            the compilation unit
	 * @param itemType
	 *            the item type
	 * @param pathToTapestryPackage
	 *            the path to tapestry package
	 * @param rootPackagePath
	 *            the root package path
	 * 
	 * @return the i tapestry item
	 * 
	 * @throws JavaModelException
	 *             the java model exception
	 */
	ITapestryItem addJava(ICompilationUnit compilationUnit,
			TapestryItemType itemType, IPath pathToTapestryPackage,
			IPath rootPackagePath) throws JavaModelException {
		String name = pathToTapestryPackage.removeFirstSegments(1)
				.removeFileExtension().toString();
		IType type = compilationUnit.getType(pathToTapestryPackage
				.removeFileExtension().lastSegment());
		if (type.isClass()) {
			IFile templateFile = findTemplateFromTapestryPackage(pathToTapestryPackage);
			return addJava(itemType, name, compilationUnit, templateFile);
		}
		return null;
	}

	/**
	 * Adds the java.
	 * 
	 * @param itemType
	 *            the item type
	 * @param name
	 *            the name
	 * @param compilationUnit
	 *            the compilation unit
	 * @param templateFile
	 *            the template file
	 * 
	 * @return the i tapestry item
	 */
	ITapestryItem addJava(TapestryItemType itemType, String name,
			ICompilationUnit compilationUnit, IFile templateFile) {
		ITapestryItem item = null;
		switch (itemType) {
		case COMPONENT:
			ITapestryComponent component = new TapestryComponent(name,
					compilationUnit, templateFile, this);
			components.addItem(component);
			item = component;
			break;
		case PAGE:
			ITapestryPage page = new TapestryPage(name, compilationUnit,
					templateFile, this);
			pages.addItem(page);
			item = page;
			break;
		default:
			break;
		}
		return item;
	}

	/**
	 * File removed.
	 * 
	 * @param file
	 *            the file
	 * 
	 * @return the i tapestry item
	 * 
	 * @throws JavaModelException
	 *             the java model exception
	 */
	ITapestryItem fileRemoved(IFile file) throws JavaModelException {
		TapestryItemKey key = createItemKey(file);
		ITapestryItem item = null;
		ITapestryContainer<? extends ITapestryItem> container = getContainer(key
				.getType());
		switch (TapestryFileType.fromResource(file)) {
		case JAVA:
			item = container.removeItem(key.getName());
			break;
		case TML:
			item = container.getItem(key.getName());
			if (item != null) {
				((AbstractTapestryItem) item).setTemplateResource(null);
			}
			break;
		}
		return item;
	}

	/**
	 * Creates the item key.
	 * 
	 * @param file
	 *            the file
	 * 
	 * @return the tapestry item key
	 */
	TapestryItemKey createItemKey(IFile file) {
		IPackageFragmentRoot root = null;
		try {
			root = getPackageFragmentRoot(file);
		} catch (JavaModelException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		if (root != null) {
			IPath pathToTapestryPackage = calculatePathToTapestryPackage(root,
					file);
			if (pathToTapestryPackage != null) {
				TapestryItemType itemType = determineItemType(pathToTapestryPackage);
				String name = pathToTapestryPackage.removeFirstSegments(1)
						.removeFileExtension().toString();
				return new TapestryItemKey(itemType, name);
			}
		}
		return null;
	}

	/**
	 * Find item.
	 * 
	 * @param file
	 *            the file
	 * 
	 * @return the abstract tapestry item
	 */
	AbstractTapestryItem findItem(IFile file) {
		ITapestryContainer<? extends ITapestryItem> container = null;
		TapestryItemKey itemKey = createItemKey(file);
		switch (itemKey.getType()) {
		case COMPONENT:
			container = components;
			break;
		case PAGE:
			container = pages;
			break;
		}
		return (AbstractTapestryItem) container.getItem(itemKey.getName());
	}

	/**
	 * Find template from tapestry package.
	 * 
	 * @param path
	 *            the path
	 * 
	 * @return the i file
	 * 
	 * @throws JavaModelException
	 *             the java model exception
	 */
	IFile findTemplateFromTapestryPackage(IPath path) throws JavaModelException {
		String rootPackage = preferences.get(
				PreferenceConstants.APPLICATION_PACKAGE_KEY,
				PreferenceConstants.APPLICATION_PACKAGE_DEFAULT);

		path = path.removeFileExtension().addFileExtension(
				TapestryFileType.TML.getExtension());

		for (IPackageFragmentRoot fragmentRoot : javaProject
				.getPackageFragmentRoots()) {
			if (isSourceRootIncluded(fragmentRoot)) {
				IPackageFragment rootPackageFragment = fragmentRoot
						.getPackageFragment(rootPackage);
				if (rootPackageFragment.exists()) {
					IPath pathToProject = rootPackageFragment.getResource()
							.getProjectRelativePath().append(path);
					IFile templateResource = project.getFile(pathToProject);
					if (templateResource.exists()) {
						return templateResource;
					}
				}
			}
		}
		return null;
	}

	/**
	 * Gets the package fragment root.
	 * 
	 * @param resource
	 *            the resource
	 * 
	 * @return the package fragment root
	 * 
	 * @throws JavaModelException
	 *             the java model exception
	 */
	IPackageFragmentRoot getPackageFragmentRoot(IResource resource)
			throws JavaModelException {
		for (IPackageFragmentRoot root : javaProject
				.getAllPackageFragmentRoots()) {
			if (isSourceRootIncluded(root)
					&& root.getResource().getProjectRelativePath().isPrefixOf(
							resource.getProjectRelativePath())) {
				return root;
			}
		}
		return null;
	}

	/**
	 * Determine item type.
	 * 
	 * @param path
	 *            the path
	 * 
	 * @return the tapestry item type
	 */
	TapestryItemType determineItemType(IPath path) {
		if (path.segmentCount() > 0) {
			if ("components".equals(path.segment(0))) {
				return TapestryItemType.COMPONENT;
			}
			if ("pages".equals(path.segment(0))) {
				return TapestryItemType.PAGE;
			}
		}
		return null;
	}

	/**
	 * Calculate path to tapestry package.
	 * 
	 * @param root
	 *            the root
	 * @param resource
	 *            the resource
	 * 
	 * @return the i path
	 */
	IPath calculatePathToTapestryPackage(IPackageFragmentRoot root,
			IResource resource) {
		String rootPackage = preferences.get(
				PreferenceConstants.APPLICATION_PACKAGE_KEY,
				PreferenceConstants.APPLICATION_PACKAGE_DEFAULT);

		IPackageFragment packageFragment = root.getPackageFragment(rootPackage);

		if (packageFragment.exists()) {

			IPath relativeToTapestryPackage = resource.getProjectRelativePath()
					.makeRelativeTo(
							packageFragment.getResource()
									.getProjectRelativePath());

			if (relativeToTapestryPackage != null) {
				if (relativeToTapestryPackage.segmentCount() == 0) {
					return relativeToTapestryPackage;
				}
				if (!relativeToTapestryPackage.segment(0).equals("..")) {
					return relativeToTapestryPackage;
				}
			}
		}

		return null;
	}

	/**
	 * Gets the container.
	 * 
	 * @param itemType
	 *            the item type
	 * 
	 * @return the container
	 */
	ITapestryContainer<? extends ITapestryItem> getContainer(
			TapestryItemType itemType) {
		switch (itemType) {
		case COMPONENT:
			return components;
		case PAGE:
			return pages;
		}
		return null;
	}

	/**
	 * Checks if is source root included.
	 * 
	 * @param root
	 *            the root
	 * 
	 * @return true, if is source root included
	 * 
	 * @throws JavaModelException
	 *             the java model exception
	 */
	public boolean isSourceRootIncluded(IPackageFragmentRoot root)
			throws JavaModelException {
		if (root.getKind() != IPackageFragmentRoot.K_SOURCE) {
			return false;
		}

		// TODO

		return true;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.googlecode.tapestry.ide.core.model.ITapestryProject#getProject()
	 */
	@Override
	public IProject getProject() {
		return project;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @seecom.googlecode.tapestry.ide.core.model.IResourceModelElement#
	 * getElementResource ()
	 */
	public IResource getElementResource() {
		return project;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.googlecode.tapestry.ide.core.model.AbstractResourceModelElement#equals
	 * ( java.lang.Object)
	 */
	@Override
	public boolean equals(Object other) {
		if (this == other) {
			return true;
		}
		if (!(other instanceof ITapestryProject)) {
			return false;
		}
		TapestryProject that = (TapestryProject) other;
		if (this.project == that.project) {
			return true;
		}
		if (this.project == null || that.project == null) {
			return false;
		}
		if (this.project.equals(that.project)) {
			return true;
		}
		return super.equals(other);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.googlecode.tapestry.ide.core.model.AbstractResourceModelElement#hashCode
	 * ()
	 */
	@Override
	public int hashCode() {
		int hashCode = 0;
		if (project != null) {
			hashCode = project.hashCode();
		}
		return 29 * hashCode + super.hashCode();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.googlecode.tapestry.ide.core.model.ITapestryProject#getComponents()
	 */
	@Override
	public ITapestryContainer<? extends ITapestryComponent> getComponents() {
		return components;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.googlecode.tapestry.ide.core.model.ITapestryProject#getPages()
	 */
	@Override
	public ITapestryContainer<? extends ITapestryPage> getPages() {
		return pages;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.googlecode.tapestry.ide.core.model.ITapestryProject#getItem(java.
	 * lang.String)
	 */
	@Override
	public <T extends ITapestryItem> ITapestryItem getItem(String name,
			Class<T> itemClass) {
		ITapestryContainer<? extends ITapestryItem> container = null;
		switch (TapestryItemType.valueOf(itemClass)) {
		case COMPONENT:
			container = components;
			break;
		case PAGE:
			container = pages;
			break;
		}
		if (container != null) {
			return container.getItem(name);
		}
		return null;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.googlecode.tapestry.ide.core.model.ITapestryProject#isTapestryResource
	 * (org.eclipse.core.resources.IResource)
	 */
	@Override
	public boolean isTapestryResource(IResource resource) {
		IPackageFragmentRoot root = null;
		try {
			root = getPackageFragmentRoot(resource);
		} catch (JavaModelException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		if (root != null) {
			IPath pathToTapestryPackage = calculatePathToTapestryPackage(root,
					resource);
			if (pathToTapestryPackage != null) {
				if (resource instanceof IFolder) {
					return true;
				} else {
					if (resource instanceof IFile) {
						return findItem((IFile) resource) != null;
					}
				}
			}
		}
		return false;
	}

	/**
	 * The Enum TapestryItemType.
	 */
	private enum TapestryItemType {

		/** The PAGE. */
		PAGE("pages"),
		
		/** The COMPONENT. */
		COMPONENT("components");

		/** The package name. */
		private String packageName;

		/**
		 * Instantiates a new tapestry item type.
		 * 
		 * @param packageName
		 *            the package name
		 */
		private TapestryItemType(String packageName) {
			this.packageName = packageName;
		}

		/**
		 * Gets the package name.
		 * 
		 * @return the package name
		 */
		public String getPackageName() {
			return packageName;
		}

		/**
		 * Value of.
		 * 
		 * @param itemClass
		 *            the item class
		 * 
		 * @return the tapestry item type
		 */
		public static TapestryItemType valueOf(Class<?> itemClass) {
			if (ITapestryPage.class.isAssignableFrom(itemClass)) {
				return PAGE;
			}
			if (ITapestryComponent.class.isAssignableFrom(itemClass)) {
				return COMPONENT;
			}
			return null;
		}

	}

	/**
	 * The Class TapestryItemKey.
	 */
	private static class TapestryItemKey {

		/** The type. */
		private TapestryItemType type;

		/** The name. */
		private String name;

		/**
		 * Instantiates a new tapestry item key.
		 * 
		 * @param type
		 *            the type
		 * @param name
		 *            the name
		 */
		public TapestryItemKey(TapestryItemType type, String name) {
			this.type = type;
			this.name = name;
		}

		/**
		 * Gets the type.
		 * 
		 * @return the type
		 */
		public TapestryItemType getType() {
			return type;
		}

		/**
		 * Gets the name.
		 * 
		 * @return the name
		 */
		public String getName() {
			return name;
		}
	}

}
